﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.NetworkInformation;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;


namespace EasyNet.Componets.Core.Domain.IdGenerator.Base36Id
{
    public class Base36IdGenerator : IIdentityGenerator
    {
        /// <summary>
        /// This value should be hardcoded; edit it to reflect the in-service date for your platform:
        /// </summary>
        private readonly DateTime _inService;

        private readonly DateTime _lastInitialized;
        private readonly Stopwatch _sw;
        private readonly RandomNumberGenerator _rng;
        private readonly MD5 _md5;
        private readonly object _lockObj = new object();
        private long _lastMicroseconds = 0;
        private long? _hostSum = null;
        private string _hostHash;
        // reserved byte, start at the max Base36 value, can decrement 
        // up to 35 times when values are exhausted (every ~115 years),
        // or repurpose as a discriminator if desired:
        private int _reserved = 35;
        private bool _shouldCheckReserved = false;
        private Regex _regex;
        private readonly Random _rnd;
        private readonly Base36Converter _base36Converter;
        public Base36IdGenerator()
        {
            _base36Converter = new Base36Converter();
            _inService = DateTime.Parse("2012-01-01 00:00:00.000");
            _lastInitialized = DateTime.Now;
            _sw = Stopwatch.StartNew();
            _rng = RandomNumberGenerator.Create();
            _md5 = MD5.Create();
            _hostHash = ComputeHostHash();
            _regex = new Regex("[^0-9a-zA-Z]");
            _rnd = new Random();

        }


        /// <summary>
        /// Generates a unique, sequential, Base36 string, 16 characters long.
        /// The first 10 characters are the microseconds elapsed since the InService DateTime 
        ///		(constant field you hardcode in this file).
        /// The next 2 characters are a compressed checksum of the MD5 of this host. 
        /// The next 1 character is a reserved constant of 36 ('Z' in Base36).
        /// The last 3 characters are random number less than 46655 additional for additional uniqueness.
        /// </summary>
        /// <returns>Returns a unique, sequential, 16-character Base36 string</returns>
        public string NewId()
        {
            return NewBase36(null);
        }


        #region 私有


        /// <summary>
        /// Generates a unique, sequential, Base36 string, 16 characters long.
        /// The first 10 characters are the microseconds elapsed since the InService DateTime 
        ///		(constant field you hardcode in this file).
        /// The next 2 characters are a compressed checksum of the MD5 of this host. 
        /// The next 1 character is a reserved constant of 36 ('Z' in Base36).
        /// The last 3 characters are random number less than 46655 additional for additional uniqueness.
        /// </summary>
        /// <param name="delimiter">If provided, formats the ID as four groups of
        /// 4 characters separated by the delimiter.</param>
        /// <returns>Returns a unique, sequential, 16-character Base36 string</returns>
        private string NewBase36(string delimiter)
        {
            // For 10 chars from timestamp, microseconds since InService, uses Stopwatch...
            long microseconds = InitStaticsReturnMicroseconds();

            // For 2 chars from host id MD5...
            string hostHash = (_hostHash ?? (_hostHash = ComputeHostHash()));

            // 1 reserved char; static largest base36 digit.
            // If the same ID system, scheme and sequence is still in use 
            // more than 115.85 years after in-service date, decrements
            // 'reserved' by 1 for each whole multiple of 115 years
            // elapsed, up to 35 times max. If the same system, scheme
            // and sequence is still in service 3,850 years from the
            // initial go-live, you probably have bigger problems than 
            // ID collisions...
            int reserved = _reserved;

            // 3 chars random in Base36 = 46656 units
            string rndHexStr = GetRandomDigits();
            StringBuilder sb = new StringBuilder();
            sb.Append(_base36Converter.FromInt64(microseconds).PadLeft(10, '0'));
            sb.Length = 10;
            sb.Append(hostHash.PadLeft(2, '0'));
            sb.Length = 12;
            sb.Append(_base36Converter.FromInt32(reserved));
            sb.Length = 13;
            sb.Append(_base36Converter.FromHex(rndHexStr).PadLeft(3, '0'));
            sb.Length = 16;
            if (!string.IsNullOrEmpty(delimiter))
            {
                sb.Insert(12, delimiter);
                sb.Insert(8, delimiter);
                sb.Insert(4, delimiter);
            }
            return sb.ToString();
        }

        /// <summary>
        /// Generates a new Base36 identifier with the 11th and 12th bits
        /// replaced by the first two characters of the value supplied for 
        /// <paramref name="overrideHostString"/>.
        /// 
        /// If <paramref name="overrideReservedByte"/> is supplied, the 
        /// reserved 13th bit (normally 'Z') will be replaced by it.
        /// 
        /// Examples of overriding the host bytes might include re-purposing 
        /// those characters for entity type identification, or for synchronizing 
        /// a group of servers to issue ids in the same range. (This last example 
        /// somewhat increases the possibility of a collision by about 1.5432 * 10⁻³). 
        /// </summary>
        /// <param name="overrideHostString"></param>
        /// <returns></returns>
        private string NewBase36(string overrideHostString, byte? overrideReservedByte = null)
        {
            if (string.IsNullOrEmpty(overrideHostString) && null == overrideReservedByte)
                return NewBase36(null);
            byte[] origBase = GetBytes(NewBase36(null));
            byte[] overrideHostBytes = GetBytes(overrideHostString);
            if (overrideHostBytes.Length > 0)
                origBase[10] = overrideHostBytes[0];
            if (overrideHostBytes.Length > 1)
                origBase[11] = overrideHostBytes[1];
            if (overrideReservedByte != null && overrideReservedByte.HasValue)
                origBase[12] = overrideReservedByte.Value;
            return GetString(origBase);
        }

        private long InitStaticsReturnMicroseconds()
        {
            long microseconds = GetMicroseconds();
            // init server name checksum...
            _hostHash = (_hostHash ?? (_hostHash = ComputeHostHash()));
            // decrement reserved byte every 115 years...
            _reserved = _shouldCheckReserved ? 35 - Convert.ToInt32(microseconds / 3656158440062975) : _reserved;
            return microseconds;
        }

        /// <summary>
        /// Return the elapsed microseconds since the in-service DateTime; will never
        /// return the same value twice. Uses a high-resolution Stopwatch (not DateTime.Now)
        /// to measure durations, and uses shared memory to ensure uniqueness.
        /// </summary>
        /// <returns></returns>
        private long GetMicroseconds()
        {
            long microseconds;
            lock (_lockObj)
            {
                do
                {
                    microseconds = (_lastInitialized.Subtract(_inService).Add(_sw.Elapsed).Ticks) / 10;
                } while (microseconds <= _lastMicroseconds + 9); // max 1 per microsecond
                _lastMicroseconds = microseconds;
            }
            _shouldCheckReserved = _lastMicroseconds > TimeSpan.FromDays(365.25 * 115).Ticks / 10;
            return microseconds;
        }


        /// <summary>
        /// Returns a random Base36 number 3 characters long. 
        /// </summary>
        /// <returns></returns>
        private string GetRandomDigits()
        {
            Int32 rndInt;
            lock (_lockObj)
            {
                rndInt = _rnd.Next(46655);
            }
            return _base36Converter.Encode(rndInt);
        }

        /// <summary>
        /// Compresses the MD5 of this server's hostname by summing the bytes until the sum
        /// fits into two Base36 characters (<= 36^2, or 1296):
        /// </summary>
        /// <returns>2 character Base36 checksum of MD5 of hostname</returns>
        private string ComputeHostHash()
        {
            string hash = _base36Converter.Encode(
                    (_hostSum.HasValue ? _hostSum.Value : (
                            _hostSum = new long?(GetBytesSum(ComputeMd5(GetHostBytes()), 36, 2))).Value));
            return hash;
        }
        /// <summary>
        /// For internal use; use the parameterless overload for effeciency.
        /// </summary>
        /// <param name="host"></param>
        /// <returns></returns>
        private string ComputeHostHash(string host)
        {
            long sum = new long?(GetBytesSum(ComputeMd5(host), 36, 2)).Value;
            string hash = _base36Converter.Encode(sum);
            return hash;
        }

        private byte[] ComputeMd5(string value)
        {
            return _md5.ComputeHash(GetBytes(value));
        }

        private byte[] ComputeMd5(byte[] value)
        {
            return _md5.ComputeHash(value);
        }

        /// <summary>
        /// Recursively sums a byte array until the value will fit within
        /// <paramref name="maxChars"/> characters in the base specified by
        /// <paramref name="forBase"/>.
        /// </summary>
        /// <param name="val">The byte array to checksum</param>
        /// <param name="maxChars">The max number of characters to hold the checksum</param>
        /// <param name="forBase">The base in which the checksum will be expressed</param>
        /// <returns></returns>
        private long GetBytesSum(byte[] val, int forBase, int maxChars)
        {
            long maxVal = Convert.ToInt64(Math.Pow(forBase, maxChars));
            byte[] arr = val;
            long tmp;
            do
            {
                tmp = arr.Aggregate<byte, long>(0, (current, t) => current + t);
                arr = BitConverter.GetBytes(tmp);
            } while (tmp > maxVal);
            return tmp; // arr.Where(b => b != 0).ToArray();
        }

        /// <summary>
        /// Returns a byte array containing the HostName if available, or
        /// else the mac address of the fastest non-loopback, non-tunnel 
        /// NIC available. If neither can be determined, will default to an 
        /// array of 6 bytes all set to 35.
        /// </summary>
        /// <returns></returns>
        private byte[] GetHostBytes()
        {
            const int MIN_MAC_ADDR_LENGTH = 6;
            byte[] defaultBytes = new byte[] { 35, 35, 35, 35, 35, 35 };
            byte[] macBytes = new byte[] { 35, 35, 35, 35, 35, 35 };
            byte[] retBytes = new byte[] { 0 };
            byte[] hostnameBytes = GetBytes(System.Net.Dns.GetHostName());
            if (hostnameBytes.Length > 3)
            {
                retBytes = hostnameBytes;
            }
            else
            {
                try
                {
                    List<NetworkInterface> candidateInterfaces =
                        NetworkInterface.GetAllNetworkInterfaces().ToList().Where(nw =>
                            nw.NetworkInterfaceType != NetworkInterfaceType.Loopback
                            && nw.NetworkInterfaceType != NetworkInterfaceType.Tunnel
                            && nw.OperationalStatus == OperationalStatus.Up
                            && nw.GetPhysicalAddress().GetAddressBytes().Length >= MIN_MAC_ADDR_LENGTH)
                        .ToList();
                    macBytes = candidateInterfaces.Max(nw => nw.GetPhysicalAddress().GetAddressBytes());
                    if (macBytes.Length >= 6)
                        retBytes = macBytes;
                }
                catch (Exception ex)
                { // back to default
                    Trace.WriteLine(ex);
                    macBytes = defaultBytes;
                    retBytes = macBytes;
                }
            }
            if ((retBytes ?? (retBytes = defaultBytes)).Length < 6)
                retBytes = retBytes.Union(defaultBytes).ToArray();
            return retBytes;
        }
        /// <summary>
        /// Shorthand for Encoding.Default.GetBytes
        /// </summary>
        /// <param name="str"></param>
        /// <returns></returns>
        private byte[] GetBytes(string str)
        {
            return Encoding.Default.GetBytes(str);
        }
        /// <summary>
        /// Shorthand for Encoding.Default.GetString
        /// </summary>
        /// <param name="bytes"></param>
        /// <returns></returns>
        private string GetString(byte[] bytes)
        {
            return Encoding.Default.GetString(bytes);
        }
        #endregion

    }
}
